﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace GameOfLife
{
    class Program
    {
        #region constants

        const char ALIVE = 'O', DEAD = ' ';
        const int X_WIDTH = 100, Y_HEIGHT = 60;
        const int DISPLAY_PAUSE = 1000;
        const int STEP_CHANGE = 10;
        
        #endregion

        #region local vars

        private static int m_iUpdateEveryMills = 500;
        private static int m_iRarityNumber = 10;
        private static char[,] m_currentState, m_nextState;
        private static bool m_bIsRunning;
        private static Random m_rnd;
        private static StringBuilder m_consoleBuffer;

        #endregion

        /// <summary>
        /// Main entry point of the program.
        /// </summary>
        /// <param name="args">Command line arguments.</param>
        static void Main(string[] args)
        {
            setupConsole();
            initializeVariables();
            displayPreface();
            populateArray(m_currentState);
            beginSimulation();
            readUserInput();
        }

        #region input/output

        /// <summary>
        /// Setup the console display.
        /// </summary>
        private static void setupConsole()
        {
            Console.Title = "Conway's Game of Life";
            Console.WindowLeft = Console.WindowTop = 0;
            Console.WindowWidth = X_WIDTH + 2;
            Console.WindowHeight = Y_HEIGHT + 2;
        }
        /// <summary>
        /// Keep reading a key from the keyboard and process it.
        /// </summary>
        private static void readUserInput()
        {
            ConsoleKeyInfo c;
            bool stayInLoop = true;
            do
            {
                c = Console.ReadKey(true);
                switch (c.Key)
                {
                    case ConsoleKey.LeftArrow:
                        m_iUpdateEveryMills -= STEP_CHANGE;
                        if (m_iUpdateEveryMills < STEP_CHANGE)
                            m_iUpdateEveryMills = STEP_CHANGE;
                        break;
                    case ConsoleKey.RightArrow:
                        m_iUpdateEveryMills += STEP_CHANGE;
                        break;
                    case ConsoleKey.C:
                        getNewRarityNumber();
                        m_bIsRunning = true;
                        goto case ConsoleKey.R;
                    case ConsoleKey.R:
                        stayInLoop = false;
                        break;
                    case ConsoleKey.Q:
                        m_bIsRunning = false;
                        break;
                }

            } while (m_bIsRunning & stayInLoop);

            if (m_bIsRunning)
            {
                // we have been asked to reset the world
                m_bIsRunning = false;
                Thread.Sleep(m_iUpdateEveryMills); //allow catchup
                zeroArray(m_currentState);
                zeroArray(m_nextState);
                populateArray(m_currentState);
                beginSimulation();
                readUserInput(); //ewww
            }
        }
        /// <summary>
        /// Display welcome message.
        /// </summary>
        private static void displayPreface()
        {
            Console.WriteLine("Hello. This is an implemtation of conway's game of life by Josh Twigg.\n\n" +
                "The following keys do things:" +
                "\n\tLeft and right arrows:Change update speed" +
                "\n\tR:to restart with new random seed" +
                "\n\tC:to change the number of ALIVE cells that get generated" +
                "\n\tQ:if you have had enough..." +
                "\n\nPress any key to continue ...");
            Console.ReadKey();
        }

        #endregion

        #region simulation functions

        /// <summary>
        /// Begin a simulation.
        /// </summary>
        private static void beginSimulation()
        {
            m_bIsRunning = true;
            ThreadStart simStart = new ThreadStart(runSimulation);
            Thread simThread = new Thread(simStart);
            simThread.Start();
        }
        /// <summary>
        /// Draws the current array to a stringbuilder and then to the console.
        /// </summary>
        private static void drawSimulation()
        {
            for (int y = 0; y < Y_HEIGHT; y++)
            {
                for (int x = 0; x < X_WIDTH; x++)
                    m_consoleBuffer.Append(m_currentState[x, y]);
                m_consoleBuffer.Append('\n');
            }
            // write info
            m_consoleBuffer.Append("UPDATE FREQ:" + m_iUpdateEveryMills + ". Rarity: 1 in every " + m_iRarityNumber);
            Console.Clear();
            Console.Write(m_consoleBuffer.ToString());
            m_consoleBuffer.Clear();
        }
        /// <summary>
        /// Main simulation loop.
        /// </summary>
        private static void runSimulation()
        {
            byte neighbourCount = 0;
            do
            {
                for (int x = 0; x < X_WIDTH; x++)
                    for (int y = 0; y < Y_HEIGHT; y++)
                    {
                        neighbourCount = countAliveNeighbours(m_currentState, x, y);
                        if (m_currentState[x, y] == ALIVE)
                        {
                            //Any live cell with fewer than two live neighbours dies, as if caused by under-population.
                            if (neighbourCount < 2)
                                m_nextState[x, y] = DEAD;
                            //Any live cell with two or three live neighbours lives on to the next generation.
                            else if (neighbourCount < 4)
                                m_nextState[x, y] = ALIVE;
                            //Any live cell with more than three live neighbours dies, as if by overcrowding.
                            else
                                m_nextState[x, y] = DEAD;
                        } 
                            //Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
                        else if (neighbourCount == 3)
                            m_nextState[x, y] = ALIVE;
                    }

                char[,] temp = m_currentState;
                m_currentState = m_nextState;
                m_nextState = temp;
                zeroArray(m_nextState); // use old current to save initializing new array each update

                drawSimulation();
                Thread.Sleep(m_iUpdateEveryMills);
            } while (m_bIsRunning);
        }

        #endregion

        #region utility functions

        /// <summary>
        /// Count how many neighbours of the specific cell are alive.
        /// </summary>
        /// <param name="array">The array of cells.</param>
        /// <param name="x">X axis position of the cell.</param>
        /// <param name="y">Y axis position of the cell.</param>
        /// <returns>The number of alive neighbours.</returns>
        private static byte countAliveNeighbours(char[,] array, int x, int y)
        {
            byte aliveCount = 0;
            /* Column one */
            if (x > 0)
            {
                if (y > 0 && array[x - 1, y - 1] == ALIVE)
                    aliveCount++;
                if (array[x - 1, y] == ALIVE)
                    aliveCount++;
                if (y < Y_HEIGHT - 1 && (array[x - 1, y + 1] == ALIVE))
                    aliveCount++;
            }
            /* Column two */
            if (y > 0)
                if (array[x, y - 1] == ALIVE)
                    aliveCount++;
            // Don't count oneself
            if (y < Y_HEIGHT - 1)
                if (array[x, y + 1] == ALIVE)
                    aliveCount++;
            /* Column three */
            if (x < X_WIDTH - 1)
            {
                if (y > 0 && array[x + 1, y - 1] == ALIVE)
                    aliveCount++;
                if (array[x + 1, y] == ALIVE)
                    aliveCount++;
                if (y < Y_HEIGHT - 1 && array[x + 1, y + 1] == ALIVE)
                    aliveCount++;
            }

            return aliveCount;
        }
        /// <summary>
        /// Set all characters in an array to DEAD.
        /// </summary>
        /// <param name="array">The array to set the elements to.</param>
        /// <param name="toChar">The character to set each element to.</param>
        private static void zeroArray(char[,] array, char toChar = DEAD)
        {
            for (int x = 0, y = 0; y < Y_HEIGHT; y = (x == X_WIDTH ? (x = 0) + (y + 1) : y))
            {
                array[x++, y] = (char)toChar;
            }
        }
        /// <summary>
        /// Populate the array with psuedo random values.
        /// </summary>
        /// <param name="array">The array to populate.</param>
        private static void populateArray(char[,] array)
        {
            for (int x = 0, y = 0; y < Y_HEIGHT; y = (x == X_WIDTH ? (x = 0) + (y + 1) : y))
                array[x++, y] = (m_rnd.Next(0, m_iRarityNumber) == 0 ? ALIVE : DEAD);
        }
        /// <summary>
        /// Initialize the local variables.
        /// </summary>
        private static void initializeVariables()
        {
            m_consoleBuffer = new StringBuilder();
            m_rnd = new Random();
            m_currentState = new char[X_WIDTH, Y_HEIGHT];
            m_nextState = new char[X_WIDTH, Y_HEIGHT];
            zeroArray(m_currentState);
            zeroArray(m_nextState);
        }
        /// <summary>
        /// Get a new number from the users.
        /// </summary>
        private static void getNewRarityNumber()
        {
            m_bIsRunning = false;
            Thread.Sleep(m_iUpdateEveryMills);
            Console.Clear();
            Console.Write("Chance of alive cell is 1 in?\n(currently 1 in " + m_iRarityNumber + ") ...");
            string input = Console.ReadLine();
            int result;
            int.TryParse(input, out result);

            if (result == 0)
                Console.WriteLine("Number not valid. Will remain " + m_iRarityNumber + '.');
            else
            {
                m_iRarityNumber = result;
                Console.WriteLine("Changed to " + m_iRarityNumber);
            }

            Thread.Sleep(DISPLAY_PAUSE);
        }

        #endregion
    }
}
